local unpack = unpack
local UI = UI

local function strval(value)
	local t = type(value)
	local s
	if t == "table" and UI.params[value] then
		t = "widget:"..UI.params[value].type
		s = UI.params[value].name or "anonymous"..tostring(value)
	else
		s = tostring(value)
	end
	return string.format("<%s:%s>", t, s)
end

UI.StrVal = strval

function UI.CheckArgType(func, index, name, types, value)
	local t = type(value)
	local r_number
	if type(types) == "string" then
		if t == types then return end
		if types == "number" then r_number = true end
	elseif type(types) == "table" then
		for _, tt in ipairs(types) do
			if tt == t then return end
			if tt == "number" then r_number = true end
		end
	elseif type(types) == "function" then
		if types(value) then return end
	end
	if r_number and t == "string" and tonumber(value) then
		return
	end
	error(string.format("In %s(), parameter %d (%s). Unexpected type %s (%s)", func, index, name, t, strval(value)))
end

local function isclass(class, ...)
	for i = 1, select("#", ...) do
		if class.name == select(i, ...) then return true end
	end
	local parent = class.parent
	if parent then
		if type(parent) == "string" then
			return isclass(UI.objects[parent], ...)
		else
			for _, p in ipairs(parent) do
				if isclass(UI.objects[p], ...) then return true end
			end
		end
	end
end

local ClassMetaTables = {}
function UI.IsWidgetType(entry, ...)
	local class = UI.params[entry].class
	if not class then return end
	return isclass(class, ...)
end

function UI.CheckArgWidget(func, index, name, type, value)
	if not UI.params[value] then
		error(string.format("In %s(), parameter %d (%s) is not a widget ! (%s)", func, index, name, strval(value)))
	end
	if not UI.IsWidgetType(value, type) then
		error(string.format("In %s(), parameter %d (%s) is not a %s ! (%s)", func, index, name, type, strval(value)))
	end
end

function UI.CheckWidgetType(func, value, type)
	return UI.CheckArgWidget(func, 1, "self", type, value)
end

local function FillMetaTable(class, table)
	local parent = class.parent
	if not table then table = {} end
	if parent then
		if type(parent) == "string" then
			FillMetaTable(UI.objects[parent], table)
		else -- assume table of strings
			for _, p in ipairs(parent) do
				FillMetaTable(UI.objects[p], table)
			end
		end
	end
	for name, value in pairs(class.prototype) do
		table[name] = value
	end
	return table
end

local function getInheritance(inherit)
	local r = {}
	local p = 1
	local append = function (e)
		local sub = inherit:sub(p, e and (e - 1))
		sub = sub:gsub("^%s+", ""):gsub("%s+$", "")
		if string.len(sub) > 0 then
			table.insert(r, sub)
		end
	end
	while true do
		local e = inherit:find(",", p, true)
		if not e then break end
		append(e)
		p = e + 1
	end
	append()
	if #r == 1 then
		return r[1]
	end
	return r
end

function UI.NewWidgetType(name, parent)
	local object = {
		name = name,
		prototype = {},
		parent = parent
	}

	function object:new(name, inherit)
		if inherit then
			inherit = getInheritance(inherit)
		end
		if not self.mt then
			self.mt = {
				__index = FillMetaTable(self),
			}
			ClassMetaTables[self.mt] = self
		end
		local r = setmetatable({ [0] = newproxy() }, self.mt)
		self:init(r, name, self.name)
		UI.params[r].inherit = inherit
		return r
	end

	function object:init(o, name, type_)
		local parent = self.parent
		if parent then
			if type(parent) == "string" then
				UI.objects[parent]:init(o, name, type_)
			else
				for _, p in ipairs(parent) do
					UI.objects[p]:init(o, name, type_)
				end
			end
		end
		if self.Initialize then
			self:Initialize(o, name, type_)
		end
		local t = UI.params[o]
		t.class = self
		t.self = o
		for key, value in pairs(self.parameters) do
			t[key] = value
		end
	end

	UI.objects[name] = object

	return object
end

function UI.MakeAccessor(strType, funcName, attrName)
	local f
	if type(attrName) == "string" then
		f = function (self)
			UI.CheckWidgetType(strType..":"..funcName, self, strType)
			return UI.params[self][attrName]
		end
	else
		local t = {}
		f = function (self)
			UI.CheckWidgetType(strType..":"..funcName, self, strType)
			local s = UI.params[self]
			for i, n in ipairs(attrName) do
				t[i] = s[n]
			end
			return unpack(t)
		end
	end
	UI.objects[strType].prototype[funcName] = f
end

function UI.MakeTypeSetter(strType, funcName, attrName, attrType)
	local f
	if type(attrName) == "string" then
		f = function (self, value)
			UI.CheckWidgetType(strType..":"..funcName, self, strType)
			UI.CheckArgType(strType..":"..funcName, 2, attrName, attrType, value)
			UI.params[self][attrName] = value
		end
	else
		local t = {}
		f = function (self, ...)
			UI.CheckWidgetType(strType..":"..funcName, self, strType)
			for i = 1, #attrName do
				UI.CheckArgType(strType..":"..funcName, i+1, attrName[i], attrType[i], select(i, ...))
			end
			local s = UI.params[self]
			for i = 1, #attrName do
				s[attrName[i]] = select(i, ...)
			end
		end
	end
	UI.objects[strType].prototype[funcName] = f
end

--[[

Some LayoutFrame metamethods here :

Layout() is used to recalculate the position of frames, if needed.
Reparent() is used to change the parent of a frame.
]]

local RootFrame = {
	GetLeft = function (self) return 0 end,
	GetRight = function (self) return self:GetWidth() end,
	GetTop = function (self) return self:GetHeight() end,
	GetBottom = function (self) return 0 end,

	GetWidth = function (self) return api.global.GetScreenWidth() * self:GetHeight() / api.global.GetScreenHeight() end,
	GetHeight = function (self) return 768 end,

	GetCenter = function (self) return self:GetWidth() / 2, self:GetHeight() / 2 end,
}

local layouting = {}

local function Layout(frame)
	if not frame then return end
	local t = UI.params[frame]
	local parent = t.parent
	local dirty = t.dirty
	if not dirty then
		for _, point in ipairs(t.points) do
			dirty = dirty or Layout(point.relativeFrame or parent)
		end
	end
	if not dirty then return end
	assert(not layouting[frame], "Layout() loop")
	if t.scale and t.scale ~= 1 then
		print("WARNING: layouting scaled frame (not supported yet)", scale, t.name)
	end
	layouting[frame] = true
	local rect_left, rect_right
	local rect_top, rect_bottom
	local rect_width, rect_height
	local rect_centerx, rect_centery
	for _, p in ipairs(t.points) do
		local x, y
		local relativeFrame = p.relativeFrame or parent or RootFrame
		local relativePoint = p.relativePoint or point
		if relativePoint == "TOPLEFT" then
			x, y = relativeFrame:GetLeft(), relativeFrame:GetTop()
		elseif relativePoint == "TOP" then
			x = relativeFrame:GetCenter()
			y = relativeFrame:GetTop()
		elseif relativePoint == "TOPRIGHT" then
			x, y = relativeFrame:GetRight(), relativeFrame:GetTop()
		elseif relativePoint == "LEFT" then
			x = relativeFrame:GetLeft()
			y = select(2, relativeFrame:GetCenter())
		elseif relativePoint == "CENTER" then
			x, y = relativeFrame:GetCenter()
		elseif relativePoint == "RIGHT" then
			x = relativeFrame:GetRight()
			y = select(2, relativeFrame:GetCenter())
		elseif relativePoint == "BOTTOMLEFT" then
			x, y = relativeFrame:GetLeft(), relativeFrame:GetBottom()
		elseif relativePoint == "BOTTOM" then
			x = relativeFrame:GetCenter()
			y = relativeFrame:GetBottom()
		elseif relativePoint == "BOTTOMRIGHT" then
			x, y = relativeFrame:GetRight(), relativeFrame:GetBottom()
		else
			error(string.format("Invalid relative anchor : %s", relativePoint))
		end
		if x then x = x + p.offsetx end
		if y then y = y + p.offsety end
		local point = p.point
		if point == "TOPLEFT" then
			rect_left = x
			rect_top = y
		elseif point == "TOP" then
			rect_centerx = x
			rect_top = y
		elseif point == "TOPRIGHT" then
			rect_right = x
			rect_top = y
		elseif point == "LEFT" then
			rect_left = x
			rect_centery = y
		elseif point == "CENTER" then
			rect_centerx = x
			rect_centery = y
		elseif point == "RIGHT" then
			rect_right = x
			rect_centery = y
		elseif point == "BOTTOMLEFT" then
			rect_left = x
			rect_bottom = y
		elseif point == "BOTTOM" then
			rect_centerx = x
			rect_bottom = y
		elseif point == "BOTTOMRIGHT" then
			rect_right = x
			rect_bottom = y
		end
	end
	if rect_left and rect_right then
		rect_width = rect_right - rect_left
	end
	if rect_top and rect_bottom then
		rect_height = rect_top - rect_bottom
	end
	if not rect_width and t.width > 0 then
		rect_width = t.width
	end
	if not rect_height and t.height > 0 then
		rect_height = t.height
	end
	if UI.IsWidgetType(frame, "FontString") then
		local h = select(2, frame:GetFont())
		if not rect_width then
			local text = t.text
			if type(text) == "function" then
				text = text()
			end
			rect_width = (text or ""):len() * (h / 2)
		end
		if not rect_height then
			rect_height = t.textheight or h
		end
	end
	if not rect_left and rect_width then
		if rect_right then
			rect_left = rect_right - rect_width
		elseif rect_centerx then
			rect_left = rect_centerx - rect_width / 2
		end
	end
	if not rect_bottom and rect_height then
		if rect_top then
			rect_bottom = rect_top - rect_height
		elseif rect_centery then
			rect_bottom = rect_centery - rect_height / 2
		end
	end

	rect_left = rect_left or 0
	rect_bottom = rect_bottom or 0
	rect_width = rect_width or 0
	rect_height = rect_height or 0

	local rect = t.rect

	local changed =
		rect.left ~= rect_left or
		rect.bottom ~= rect_bottom or
		rect.width ~= rect_width or
		rect.height ~= rect_height

	rect.left = rect_left
	rect.bottom = rect_bottom
	rect.width = rect_width
	rect.height = rect_height

	layouting[frame] = nil
	t.dirty = false

	if changed then
		UI.CallScript(frame, "OnSizeChanged", rect_width, rect_height)
	end
	return changed
end

local function HasChildren(parent, child)
	local ptable = UI.IsWidgetType(parent, "Frame") and "children" or "regions"
	local children = UI.params[parent][ptable]
	if not children then return end
	for frame in pairs(children) do
		if frame == child or HasChildren(frame, child) then
			return true
		end
	end
end

local function Reparent(frame, parent)
	if type(parent) == "string" then
		parent = UI.GetGlobal(parent)
	end
	local t = UI.params[frame]
	local old = t.parent
	local ptable = UI.IsWidgetType(frame, "Frame") and "children" or "regions"
	if old ~= parent then
		assert(not HasChildren(frame, parent), "parenting to a children")
		if old then
			local pt = UI.params[old]
			pt[ptable][frame] = nil
		end
		if parent then
			assert(UI.IsWidgetType(parent, "Frame") and true or false)
			local pt = UI.params[parent]
			if not pt[ptable] then
				pt[ptable] = setmetatable({}, { __mode = "k" })
			end
			pt[ptable][frame] = true
		end
		t.parent = parent
	end
end

local function Visible(frame)
	local t = UI.params[frame]
	if not t or not t.visible then
		return false
	end
	if t.parent then
		return Visible(t.parent)
	end
	return true
end

local function GetParent(frame)
	return UI.params[frame].parent
end

local function CallScript(frame, script, ...)
	if config.DELAY_ONLOAD and UI.framesToLoad[frame] then
		if script == "OnLoad" then
			UI.framesToLoad[frame] = nil
		else
			UI.CallScript(frame, "OnLoad")
		end
	end
	local t = UI.params[frame]
	if not t or not t.scripts then
		return
	end
	if script == "OnUpdate" and not Visible(frame) then
		return
	end
	local func = t.scripts[script]
	if not func then return end
	local params = {...}
	Security:setfromobject(func, "<unknown>", "INSECURE")
	api.call(function () func(frame, unpack(params)) end)
	local hooks = t.scripthooks and t.scripthooks[script]
	if hooks then
		local taint, level = Security.taint, Security.level
		for _, handler in ipairs(hooks) do
			Security:setfromobject(handler, "<unknown>", "INSECURE")
			api.call(function () handler(frame, unpack(params)) end)
		end
		Security.taint, Security.level = taint, level
	end
end

local SETTEXTURETEMPLATE = [[
return function (self, texture, ...)
	UI.CheckWidgetType(%q, self, %q)
	local t_tex = type(texture)
	if t_tex == "number" then
		local path = string.format("COLOR:%%d:%%d:%%d:%%d", texture, select(1, ...) or 0, select(2, ...) or 0, select(3, ...) or 0)
		texture = UI.CreateObject"Texture"
		texture:SetTexture(path)
		texture:SetAllPoints()
	elseif t_tex ~= "string" then
		UI.CheckArgWidget(%q, 2, "texture", "Texture", texture)
	else
		local path = texture
		texture = UI.CreateObject"Texture"
		texture:SetTexture(path)
		texture:SetAllPoints()
	end
	local t = UI.params[self]
	t[%q] = texture
	if texture then
		UI.Reparent(texture, self)
		texture:SetDrawLayer("ARTWORK")
		texture:%s()
	end
end
]]

local GETTEXTURETEMPLATE = [[
return function (self)
	UI.CheckWidgetType(%q, self, %q)
	local t = UI.params[self]
	return t[%q]
end
]]

local function MakeTextureFunctions (tbl, objType, name, default)
	local tname = "texture_"..name
	local sname = objType..":Set"..name.."Texture"
	local gname = objType..":Get"..name.."Texture"
	tbl.prototype["Set"..name.."Texture"] = loadstring(SETTEXTURETEMPLATE:format(sname, objType, sname, tname, default and "Show" or "Hide"))()
	tbl.prototype["Get"..name.."Texture"] = loadstring(GETTEXTURETEMPLATE:format(gname, objType, tname))()
end

local function SetDirtyLayout(frame)
	local t = UI.params[frame]
	if t.dirty then return end
	t.dirty = true
	for other in pairs(t.layout_children) do
		SetDirtyLayout(other)
	end
end

local function LockFrames(value)
	for _, t in pairs(UI.params) do
		t.locked = value
	end
end

UI.Layout = Layout
UI.SetDirtyLayout = SetDirtyLayout
UI.Reparent = Reparent
UI.GetParent = GetParent
UI.Visible = Visible
UI.CallScript = CallScript
UI.MakeTextureFunctions = MakeTextureFunctions
UI.LockFrames = LockFrames

function UI.DumpFrameHierarchy(visible)
	local o = io.open("frames.txt", "w")

	local dumped = {}
	for f, t in pairs(UI.params) do
		if not UI.templates[t.name] and not UI.fonts[t.name] and (not visible or t.visible) then
			dumped[f] = t
		end
	end
	local function dump_children(offset, parent)
		for f, t in pairs(dumped) do
			if t.parent == parent then
				local s = string.format("%s%s (%s%s)", t.protected and "*" or "", tostring(t.name), t.type, t.virtual and "[virtual]" or "")
				if t.rect then
					s = s..string.format(" : %d-%d %dx%d", t.rect.left, t.rect.bottom, t.rect.width, t.rect.height)
				end
				if UI.IsWidgetType(f, "FontString") and not t.text then
					s = nil
				elseif UI.IsWidgetType(f, "FontString", "Button", "EditBox") and t.text then
					s = s..string.format(" TEXT=%q", tostring(t.text))
				elseif UI.IsWidgetType(f, "Texture") and t.texture then
					s = s..string.format(" TEXTURE=%q", tostring(t.texture))
				end
				if s then
					if not t.visible then
						s = "("..s..")"
					end
					o:write(offset..s.."\n")
				end
				dumped[f] = nil
				dump_children(offset.."\t", f)
			end
		end
	end
	dump_children(o, "", nil, dumped)
	if not visible then
		for f, t in pairs(dumped) do
			o:write(string.format("<!-- !!! Frame %s (%s) not in hierarchy ! (%s) -->\n", tostring(f), tostring(t.name), tostring(t.parent)))
		end
	end
	o:close()
end

function UI.DumpFrames(file, generator)
	local out = generator.new(RootFrame:GetWidth(), RootFrame:GetHeight())
	out:open(file)
	for _, t in pairs(UI.params) do
		if not UI.templates[t.name] and not UI.fonts[t.name] and not t.parent then
			out:add(t.self)
		end
	end
	out:close()
end
